package com.qf.business.searc.service.impl;

import com.qf.business.searc.service.ISearchService;
import com.qf.business.search.input.SearchParamsInput;
import com.qf.common.core.base.R;
import com.qf.common.core.utils.DateUtils;
import com.qf.data.entity.hotel.CCity;
import com.qf.data.entity.hotel.CHotel;
import com.qf.data.entity.hotel.CPrice;
import com.qf.data.entity.hotel.CRoom;
import com.qf.data.entity.hotel.dto.CHotelDto;
import com.qf.data.entity.hotel.dto.CRoomDto;
import com.qf.feign.business.hotel.CityFeign;
import lombok.extern.slf4j.Slf4j;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FieldValueFactorFunctionBuilder;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.*;

@Service
@Slf4j
public class SearchServiceImpl implements ISearchService {

    @Autowired
    private ElasticsearchRestTemplate restTemplate;

    @Autowired
    private CityFeign cityFeign;

    /**
     * 创建索引库
     * @return
     */
    @Override
    public boolean createIndex() {
        IndexOperations operations = restTemplate.indexOps(CHotelDto.class);
        //创建索引
        boolean flag = operations.create();
        if (flag) {
            //创建映射关系
            Document document = operations.createMapping();
            operations.putMapping(document);
        }
        return flag;
    }

    /**
     * 判断索引库是否存在
     * @return
     */
    @Override
    public boolean existsIndex() {
        IndexOperations operations = restTemplate.indexOps(CHotelDto.class);
        return operations.exists();
    }

    /**
     * 新增酒店
     * @param cHotel
     * @return
     */
    @Override
    public int insertHotel(CHotel cHotel) {
        //cHotel转换成CHotelDto
        CHotelDto cHotelDto = new CHotelDto();
        BeanUtils.copyProperties(cHotel, cHotelDto);

        //处理城市
        R<CCity> r = cityFeign.queryById(cHotel.getCId());
        Assert.notNull(r.getData(), "城市信息有误！");
        cHotelDto.setCityName(r.getData().getCityName());

        //处理经纬度坐标
        Double[] location = new Double[2];
        location[1] = cHotel.getHotelLat();//设置纬度
        location[0] = cHotel.getHotelLon();//设置经度
        cHotelDto.setLocation(location);

        //保存到ES中
        restTemplate.save(cHotelDto);
        return 1;
    }

    /**
     * 新增酒店下的客房
     * @param cRoom
     * @return
     */
    @Override
    public int insertRoom(CRoom cRoom) {

        CRoomDto cRoomDto = new CRoomDto();
        BeanUtils.copyProperties(cRoom, cRoomDto);

        //将CRoomDto转换成ES脚本认识的Map集合对象
        Map<String, Object> objMap = new HashMap<>();
        objMap.put("rId", cRoomDto.getRId());
        objMap.put("roomName", cRoomDto.getRoomName());
        objMap.put("roomContent", cRoomDto.getRoomContent());
        objMap.put("prices", cRoomDto.getPrices());

        //准备一个脚本参数
        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("room", objMap);

        UpdateQuery updateQuery = UpdateQuery
                .builder(cRoom.getHId() + "") //参数id表示需要修改的文档id
                .withScript("ctx._source.rooms.add(params.room)")//设置修改的脚本 - painless脚本, ctx是修改场景下的上下文对象
                .withParams(paramsMap)
                .build();

        //执行索引Document修改，参数二表示修改哪个索引库
        restTemplate.update(updateQuery, IndexCoordinates.of("hotel-index"));
        return 1;
    }

    /**
     * 新增客房内的价格信息
     * @param hid - 酒店
     * @param cPrice
     * @return
     */
    @Override
    public int insertRoomPrice(Integer hid, CPrice cPrice) {

        Map<String, Object> priceMap = new HashMap<>();
        priceMap.put("pId", cPrice.getPId());
        priceMap.put("pDate", cPrice.getPDate().getTime());
        priceMap.put("pHasNumber", cPrice.getPHasNumber());
        priceMap.put("pPrice", cPrice.getPPrice());

        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("rid", cPrice.getRId());
        paramsMap.put("price", priceMap);

        //修改查询的条件对象
        UpdateQuery updateQuery = UpdateQuery.builder(hid + "")
                .withScript("for(room in ctx._source.rooms){ if(room.rId == params.rid){room.prices.add(params.price)}}")
                .withParams(paramsMap)
                .build();

        restTemplate.update(updateQuery, IndexCoordinates.of("hotel-index"));
        return 1;
    }

    /**
     * 修改对应的客房的某一天的价格
     * @param hid
     * @param cPrice
     * @return
     */
    @Override
    public int updateRoomPrice(Integer hid, CPrice cPrice) {

        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("rid", cPrice.getRId());
        paramsMap.put("pid", cPrice.getPId());
        paramsMap.put("pPrice", cPrice.getPPrice());
        paramsMap.put("pHasNumber", cPrice.getPHasNumber());

        //修改查询的条件对象
        UpdateQuery updateQuery = UpdateQuery.builder(hid + "")
                .withScript("for(room in ctx._source.rooms){ if(room.rId == params.rid){ for(price in room.prices){if(price.pId==params.pid){price.pPrice=params.pPrice;price.pHasNumber=params.pHasNumber;}}}}")
                .withParams(paramsMap)
                .build();

//        for(room in ctx._source.rooms){
//            if(room.rId == params.rid){
//                for(price in room.prices){
//                    if(price.pId==params.pid){
//                        price.pPrice=params.pPrice;
//                        price.pHasNumber=params.pHasNumber;
//                    }
//                }
//            }
//        }

        restTemplate.update(updateQuery, IndexCoordinates.of("hotel-index"));
        return 1;
    }

    /**
     * 计算平均价格的脚本
     */
    private String avgScript = "double endAvgPrice = Integer.MAX_VALUE;" +
            "for(room in params._source.rooms){" +
            "double avgPrice = 0.0;" +
            "double allPrice = 0.0;" +
            "double days = 0;" +
            "for(price in room.prices){" +
            "if(price.pDate >= params.beginDate && price.pDate < params.endDate){" +
            "allPrice += price.pPrice;" +
            "days++;" +
            "}" +
            "}" +
            "avgPrice = allPrice / days;" +
            "if(avgPrice >= params.minPrice && avgPrice <= params.maxPrice){" +
            "endAvgPrice = Math.min(endAvgPrice, avgPrice);" +
            "}" +
            "}" +
            "return endAvgPrice == Integer.MAX_VALUE ? -1.0 : endAvgPrice;";

    /**
     * 计算距离的脚本
     */
    private String distanceScript = "doc.location.planeDistance(params.lat, params.lon) / 1000";

    /**
     * 酒店的相关搜索
     *
     * ES文档结构：
     *
     * Document
     * -> 酒店
     *    -> 客房1 - 190
     *      -> 2021-10-1 199 5
     *      -> 2021-10-2 100 0
     *      -> 2021-10-3 299 5
     *      .....
     *    -> 客房2 - 180
     *      -> 2021-10-1 199 5
     *      -> 2021-10-2 199 5
     *      -> 2021-10-3 199 0
     *      .....
     *
     *  入住时间段  最小价格 ~ 最大价格 185 ~ 200
     *
     * 1、关键词搜索哪些字段？
     * 2、入店时间 ~ 离店时间 10-1 ~ 10-4
     * 3、搜索出的价格是什么价格？
     * 4、价格区间？ - 时间范围内并且在用户价格区间中的最低的均价
     *
     * @param params
     * @return
     */
    @Override
    public List<CHotelDto> searchHotel(SearchParamsInput params) {

        //参数给与默认值
        if(params.getBeginDate() == null){
            params.setBeginDate(DateUtils.getNowDate(0));
        }

        if (params.getEndDate() == null) {
            Calendar cal = Calendar.getInstance();
            cal.setTime(params.getBeginDate());
            cal.add(Calendar.DAY_OF_MONTH, 1);
            params.setEndDate(cal.getTime());
        }

        if (params.getMinPrice() == null) {
            params.setMinPrice(BigDecimal.ZERO);
        }

        if (params.getMaxPrice() == null) {
            params.setMaxPrice(BigDecimal.valueOf(Integer.MAX_VALUE));
        }

        log.debug("[search-hotel] - 酒店搜索，参数：{}", params);

        //--------查询时间范围内，有空余方法的客房
        QueryBuilder pricesQuery = QueryBuilders.boolQuery()
                .must(QueryBuilders.rangeQuery("rooms.prices.pDate")
                        .gte(params.getBeginDate().getTime())
                        .lt(params.getEndDate().getTime()))
                .must(QueryBuilders.termQuery("rooms.prices.pHasNumber", 0));

        QueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("rooms.prices",
                pricesQuery, ScoreMode.Avg);

        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
                .mustNot(nestedQueryBuilder);

        QueryBuilder roomsQuery = QueryBuilders.nestedQuery("rooms",
                boolQuery, ScoreMode.Avg);
        //------查询时间范围内，有空余方法的客房

        //查询城市的Query
        QueryBuilder cityQuery =
                QueryBuilders.multiMatchQuery(params.getCityName())
                    .field("cityName", 1.2f)
                    .field("cityName.pinyin", 1.0f);

        //关键词匹配查询
        QueryBuilder keywordHotelQuery =
                QueryBuilders.multiMatchQuery(params.getKeyword(), "hotelName", "hotelName.pinyin", "hotelRegx", "hotelRegx.pinyin", "hotelAddress", "hotelContent", "hotelKeyword", "hotelKeyword.pinyin");

        //关键词匹配客房
        QueryBuilder keywordRoomQuery =
                QueryBuilders.nestedQuery("rooms",
                        QueryBuilders.multiMatchQuery(params.getKeyword(),
                                "rooms.roomName", "rooms.roomName.pinyin", "rooms.roomContent"),
                        ScoreMode.Avg);

        //主查询 - Bool查询
        BoolQueryBuilder mainQuery = new BoolQueryBuilder()
                .must(roomsQuery);//查询时间范围内有客房的酒店

        //关键字不为null
        if (!StringUtils.isEmpty(params.getKeyword())) {
            mainQuery
                    .should(keywordHotelQuery)//关键词匹配酒店
                    .should(keywordRoomQuery)//关键词匹配客房
                    .minimumShouldMatch(1);
        }

        //城市不为null
        if (!StringUtils.isEmpty(params.getCityName())) {
            mainQuery
                    .must(cityQuery);//城市查询
        }

        //评分查询 - 自定义文档评分
        FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(mainQuery, new FieldValueFactorFunctionBuilder("djl").setWeight(10));


        //-----------------------设置脚本字段 - avgPrice
        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("beginDate", params.getBeginDate().getTime());
        paramsMap.put("endDate", params.getEndDate().getTime());
        paramsMap.put("minPrice", params.getMinPrice().doubleValue());
        paramsMap.put("maxPrice", params.getMaxPrice().doubleValue());

        Script script = new Script(
                ScriptType.INLINE,
                "painless",
                avgScript,
                paramsMap
        );
        ScriptField avgScriptField = new ScriptField("avgPrice", script);
        //-----------------------设置脚本字段 - avgPrice

        //-----------------------设置脚本字段 - distance
        paramsMap.put("lat", params.getLat());
        paramsMap.put("lon", params.getLon());

        Script script2 = new Script(
                ScriptType.INLINE,
                "painless",
                distanceScript,
                paramsMap
        );
        ScriptField distanceScriptField = new ScriptField("distance", script2);
        //-----------------------设置脚本字段 - avgPrice


        //构建一个原生查询
        NativeSearchQuery query = new NativeSearchQuery(functionScoreQuery);
        //添加脚本字段
        query.addScriptField(avgScriptField, distanceScriptField);
        //设置查询结果集的字段过滤
        SourceFilter sourceFilter = new FetchSourceFilter(null, new String[]{"rooms"});
        query.addSourceFilter(sourceFilter);

        //执行查询
        SearchHits<CHotelDto> searchResult = restTemplate.search(query, CHotelDto.class);
        log.debug("[search-hotel] - 酒店搜索完成，搜索结果数量{}", searchResult.getTotalHits());

        //查询结果集
        List<CHotelDto> result = new ArrayList<>();
        for (SearchHit<CHotelDto> searchHit : searchResult.getSearchHits()) {
            CHotelDto hotelDto = searchHit.getContent();

            //平均价格
            double avgPrice = hotelDto.getAvgPrice();
            if (avgPrice != -1) {
                //距离
                double distance = hotelDto.getDistance();
                DecimalFormat decimalFormat = new DecimalFormat("###.##");
                hotelDto.setAvgPrice(BigDecimal.valueOf(avgPrice).setScale(2, RoundingMode.HALF_DOWN).doubleValue());
                hotelDto.setDistance(Double.parseDouble(decimalFormat.format(distance)));
                result.add(hotelDto);
            }
        }

        return result;
    }

    /**
     * 新增酒店对应的点击量
     * @param hid
     * @param djl
     * @return
     */
    @Override
    public int incrHotelDjl(Long hid, Integer djl) {

        Map<String, Object> params = new HashMap<>();
        params.put("djl", djl);

        UpdateQuery updateQuery = UpdateQuery.builder(hid + "")
                .withScript("ctx._source.djl += params.djl")
                .withParams(params)
                .build();

        restTemplate.update(updateQuery, IndexCoordinates.of("hotel-index"));
        return 1;
    }
}
